/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is the ViewMyCurrency extension.
 *
 * The Initial Developer of the Original Code is
 * Will Moffat.
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

//dump("// $Id: WILL_WebServices.js,v 1.31 2007/01/22 09:50:52 Will Exp $;\n");

VMC.webServiceList = new Array();
VMC.webServiceList.getServiceFromName = function(name) {
  for (var i=0;i<this.length;i++) {
    if (this[i].getName()==name) { return this[i]; }  //WILL: TODO: getName() --> toString()
  }
  VMC.log('ERR','Tried to retrieve  unknown webservice '+name);
  return null;
};

VMC.webServiceList.applyFunc = function(func) {
  for (var i=0;i<this.length;i++) {
    func(this[i]);
  }
};

VMC.webServiceList.assertHasWebService = function(item,failSilently) {
  var error;
  if (!item.webService)                         { error='Item:'+item+' does not have a webservice'; }
  else if (!item.webService.getUpdateUrl)       { error='Item:'+item+'s webservice '+item.webService+' is missing getUpdateUrl()'; }
  else if (!item.webService.getRateFromContent) { error='Item:'+item+'s webservice '+item.webService+' is missing getRateFromContent()'; }

  if (error && !failSilently) { VMC.log('ERR',error); }
  if (error) { return false; }
  return true;
};



// A web service class must have these functions:
// getUpdateUrl(fromCode,toCode)
// getRateFromContent(content)
// + array supportedCodes

VMC.initWebServices = function(webServicesList) {
  var WebServiceX = function() {};
  var Google      = function() {};
  //var Fixed       = function() {};

  webServicesList.push(Google,WebServiceX /*,Fixed*/); //google has more decimal places, haven't checked if it's more accurate //WILL: fixed last (otherwise it tried to take EUR)

  var googleOnly_codes = 
    [ 'CSD',   // not supported by WebserviceX or Yahoo!
      'RON' ]; // not supported by WebserviceX
  
//   var fixedOnly_codes = 
//   [ 'EUR', // WILL: THINK ABOUT: I really need to split this into supported from and to codes
//     'ATS', 'BEF', 'FIM', 'FRF', 'DEM', 'GRD', 'NLG', 'IEP', 'ITL', 'LUF', 'PTE', 'ESP', 'VAL' ]; // Old Eurozone codes

  var webServiceXonly_codes =
    ['ALL','ANG','AWG','BBD','BDT','BHD','BIF','BMD','BND','BSD','BTN','BZD','CRC','CYP','DOP','DZD','ETB','GHC','GMD','GNF','GTQ','HNL',
     'HTG','ISK','JMD','JOD','KES','KMF','KWD','KYD','KZT','LBP','LKR','LSL','LVL','MDL','MKD','MNT','MOP','MRO','MTL','MUR','MVR','MWK',
     'NAD','NGN','NIO','NPR','OMR','PAB','PGK','PYG','QAR','SCR','SDD','SLL','SVC','SZL','TND','TOP','TTD','TZS','UAH','UGX','UYU','VND',
     'VUV','WST','XAF','XCD','XOF','XPF','YER','ZMK'];

  var webServiceX_Google_codes=
    ['AED','ARS','AUD','BGN','BOB','BRL','CAD','CHF','CLP','CNY','COP','CZK','DKK','EEK','EGP','EUR','GBP','HKD','HRK','HUF','IDR','ILS',
     'INR','JPY','KRW','LTL','MAD','MXN','MYR','NOK','NZD','PEN','PHP','PKR','PLN','RUB','SAR','SEK','SGD','SIT','SKK','THB',
     'TRY','TWD','USD','VEB','ZAR'];

  webServicesList.notSupportedCodes =
    [
     'AFN','AMD','AOA','AZN','BAM','BGL','BWP','BYR','CDF','CUC','CVE','DJF','ERN','FJD','FKP','GEL','GGP','GIP','GYD','IMP',
     'IQD','IRR','JEP','KGS','KHR','KPW','LAK','LRD','LYD','MGA','MMK','MZM','RWF','SBD','SHP','SOS','SRD','STD','SYP','TJS','TMM','UZS','ZWD' ];
  
////////////////////////////////////////////////////
// WebServiceX webservice
///////////////////////////////////////////////////

  WebServiceX.getName = function() { return "WebServiceX"; };
  WebServiceX.supportedCodes = [].concat(webServiceXonly_codes,webServiceX_Google_codes);


WebServiceX.getUpdateUrl = function(fromCode,toCode) {
  return 'http://www.webservicex.net/CurrencyConvertor.asmx/ConversionRate?FromCurrency='+fromCode+'&ToCurrency='+toCode;
};

WebServiceX.getRateFromContent = function(content) {
  //  dump('content=['+content+']'); //raw HTML will mess up the JS console

  var parser    = new DOMParser();
  var xmlDoc    = parser.parseFromString(content,"application/xml");
  if (!xmlDoc) { throw 'Could not create DOM from '+xml; }
  
  var rate_text = xmlDoc.getElementsByTagName('double')[0];
  if (rate_text===undefined) { throw 'Could not get rate in '+content; }
  
  // we found the XML double tag, now get the text
  var rateStr=rate_text.textContent;

  return VMC.ExchangeRate.parseValue(rateStr);
};
  
////////////////////////////////////////////////////
// Google Webservice class - only used as static
///////////////////////////////////////////////////

 Google.getName = function() { return "Google"; };
 Google.supportedCodes = [].concat(googleOnly_codes,webServiceX_Google_codes);

Google.getUpdateUrl = function(fromCode,toCode) {
  return 'http://www.google.com/search?q=1+'+fromCode+'+in+'+toCode;
};

// You cannot do DOM parsing of HTML without creating a browser window. See: http://groups.google.com/group/netscape.public.mozilla.dom/browse_thread/thread/126739f8904f94d9/7c5cd3d4ad884074
// so I just do text matching. It's isn't actually that bad.
Google.getRateFromContent = function(content) {
  //dump('content=['+content+']'); //raw HTML will mess up the JS console
  if (!content.match(/images\/calc/)) { throw "Could not find calculator icon in Google search results"; }
  var stripped;
  stripped = content.replace(/<(.|\s)+?>/g,''); // remove all HTML tags
  stripped = stripped.replace(/&.+?;/g,'');     // remove all &stuff; - not needed but nicer for debugging
  //dump('stripped=['+stripped+']\n');

  var matches=/1 .+ = (\d*\s?\d+\.?\d*)/.exec(stripped); //WILL: This doesn't enforce thousands grouping. TODO: consider using Number parsing function?
  if (!matches) { throw "Could not find '1 xxx = y yyy.zzzzzz' in ["+stripped+"]"; }
  var rateStr=matches[1];
  rateStr = rateStr.replace(/\s/g,''); // remove whitespace (thousands separator)

  return VMC.ExchangeRate.parseValue(rateStr);
};

// Google: document.getElementsByTagName('b')[4].textContent
//var calc=document.getElementsByTagName('img')[4]
//check calc.src='http://www.google.com/images/calc_img.gif';

////////////////////////////////////////////////////
// Fixed Rate webservice
///////////////////////////////////////////////////
//   Fixed.getName = function() { return "Fixed"; };
//   Fixed.supportedCodes = fixedOnly_codes;
//   Fixed.getUpdateUrl = function(fromCode,toCode) { return 'chrome://viewmycurrency/fixedExchangeRates.txt' }; //ignore fromCode and toCode
//   Fixed.getRateFromContent = function(content,fromCode,toCode) { return 20  }; //hack!!


};


// WILL: TODO: write a yahoo web service

//var yahooSupported =
//  ['BYR','DJF','ERN','FKP','GIP','IRR','KHR','LAK','LYD','MZM','RWF','SHP','SOS','STD','SYP','ZWD']; //+ all Google ones (except CSD)

VMC.initWebServices(VMC.webServiceList);

/******************** END OF WEB SERVICES *********************************/


// WILL: cache is currenly only used for getUrl and setRate
// WILL: items are rateObj's
// Needs: 
// * setFromContentCallback(item,content) which returns success/failure
// * setFailureCallback(item,errorStr);
// * list of items
//   - item.getUpdateUrl()

VMC.WebUpdater = function() {
  var self=this;
  //public funcs
  this.test = test; // For testing a webservice without modifying items
  this.start = start;
  this.numItemsToDo = function() { return this._itemsToDo.length; };
  this.fractionLeft = function() { return this._itemsToDo.length / this._origItemCount; };
  //private funcs
  this._startNextUpdate = _startNextUpdate;
  this._onLoadContent      = _onLoadContent;
  this._notifyEnd       = _notifyEnd;

  //private vars
  this._setFromContentCallback = null;
  this._setFailureCallback = null;

  this.isBusy       = false; // WILL: TODO: make this gettable only from outside
  this._itemsToDo   = [];    // list of rates to be downloaded from web
  this._origItemCount = 0;   // how many items were we asked to do
  this._hadErrors   = false; // was there an error?
  this._currentItem = null;  // the currently downloading rate
  
  // For testing a webservice without modifying items. Does not use webService of item, but the version passed as arg
  function test(item,webService) {
    var from=item.fromCurrency.code;
    var to=item.toCurrency.code;
    if (!from || !to) { VMC.log('ERR','Missing from or to code in '+item); }
    var url       = webService.getUpdateUrl(from,to);
    var parseFunc = webService.getRateFromContent;

    if (this.isBusy) { VMC.log('ERR','WebUpdater is already busy'); }    
    if (!url)        { VMC.log('ERR','Missing url'); }
    if (!parseFunc)  { VMC.log('ERR','Missing parse function'); }

    var rate;
    try {
      var request = new XMLHttpRequest(); 
      request.open("GET", url, false); // blocking
      //dump(url+'\n');  // to monitor web access
      request.send(null); //null body
      if (request.status != 200) { return false; }// we didn't get an OK
      rate = parseFunc(request.responseText); // might throw an exception
    } catch (e) {
      VMC.log('WARN','test webservice failed with error: '+e);
      return false;
    }

    return VMC.ExchangeRate.isSaneValue(rate);
  }

  // stopOnFirstFailure - the first time a url fails to load or we can't parse the page then stop
  //                      used by initial webupate to delay full update until internet connection is made
  function start(itemsToDo,setFromContentCallback,setFailureCallback,stopOnFirstFailure) {
    //dump('ViewMyCurrency: WebUpdate starting for '+itemsToDo+'\n');
    VMC.log('INFO','  ------------ Web update starting ------------ ');
    if (this.isBusy) { VMC.alert('WebUpdater is already busy'); }
    this._setFromContentCallback = setFromContentCallback;
    this._setFailureCallback = setFailureCallback;

    this._stopOnFirstFailure = false;
    if (stopOnFirstFailure) {this._stopOnFirstFailure = true;} // explicilty set it to false if undefined

    this._itemsToDo=[];
    for (var i=0; i<itemsToDo.length; i++) { this._itemsToDo[i] = itemsToDo[i]; } //duplicate the array
    this._origItemCount = itemsToDo.length;
    //WILL: is this logic needed? for (var i=0; i<this._itemsToDo.length; i++) { this._itemsToDo[i].removeUpdateError(); }
    this.isBusy=true;
    this._notify('START');
    this._hadErrors=false;
    //kick-off the cycle _startNextUpdate --> (request.send) --> _onLoadContent --> back to _startNextUpdate()
    setTimeout( function() { self._startNextUpdate.call(self); } ,  100 ); //wait 10th of a sec and then start in a new thread
  }

  function _notifyEnd() {
    if (this._hadErrors) {
      this._notify('END_ERRORS');
      VMC.log('INFO','  -----Web update complete - HAD ERRORS ! ----- ');
    } else {
      VMC.log('INFO','  ---------- Web update complete OK ----------- ');
      this._notify('END_OK');
    }
    this.isBusy=false; //mark the updater as finished
  }

  function _startNextUpdate() {
    if (this._itemsToDo.length===0) {
      this._notifyEnd();
      return; //don't start any more requests
    }

    var itemForUpdate = this._itemsToDo.shift(); //get the next item to process
    this._currentItem=itemForUpdate;

    if (!VMC.webServiceList.assertHasWebService(this._currentItem,true)) { //failSilently
      //fake an error
      var err = { name:'Has no webservice' };
      this._onLoadContent(err); //WILL: onLoadContent call this func again. Is recursion a problem?
      return;
    }

    var url=itemForUpdate.getUpdateUrl();
    if (!url) { VMC.log('ERR',itemForUpdate+' missing update URL'); }

    VMC.log('INFO','Updating '+itemForUpdate+' from: '+url);
  
    var request = new XMLHttpRequest(); 
    // You can sometimes add  your own properties to an object with Javascript - great! But not here :-(
    //request.VMC.rateObj=rateObj;
    request.onload  =  function(evt) { self._onLoadContent.call(self,evt); };
    request.onerror =  function(evt) { self._onLoadContent.call(self,evt); }; //WILL: what kind of event is passed to onLoad?

    try {
      request.open("GET", url, true); // async
      //dump(url+'\n');  // to monitor web access
      request.send(null); //null body
    } catch (e) {
      this._onLoadContent(e); // use the same handler even if an exception is immediately raised
    }
  }

  //we assume that this is the request that came from the last xmlHttpRequest
  function _onLoadContent(event) {

    // helper func
    var summarizeError = function() {
      var errorStr;
      errorStr=request.statusText+' ('+request.status+') ';
      errorStr=errorStr + request.responseText.replace(/<[^>]*?>/g,'');  //remove all tags
      errorStr=errorStr.replace(/\n\n+/g,'\n'); //remove multiple newlines
      return errorStr;
    };

    var exception;
    try {
      if (!event)        { throw "Missing event!"; }
      if (event.name)    { throw event.name; }  // "Got an immediate exception"
      if (!event.target) { throw "Missing target"; }
      if (event.type == 'error') { throw 'Page failed to load'; }

      var request=event.target;

      if (request.status === undefined) { throw "Missing status"; }
      if (request.status != 200 && request.status !== 0) { throw "not status 200 OK or 0 (file load)"+summarizeError(); }

      var errorMsg = this._setFromContentCallback(this._currentItem,request.responseText);
      if (errorMsg) { throw errorMsg; }

      // else we successfully updated
      VMC.log('INFO','Updated OK from web: '+this._currentItem); //WILL: TODO: move log to callback
      VMC.log('DBG',request.responseText);
      //dump(request.responseText+'\n\n'); // to monitor web access
      this._notify('RATE_UPDATED', this._currentItem.toString() );
      
    } catch(e) { exception=e; }
      
    if (exception) {
      VMC.log('WARN','WebUpdate caught an exception: '+exception);
      this._setFailureCallback(this._currentItem,exception);
      this._hadErrors=true;
      this._notify('RATE_ERROR'); //report each time
    }
    this._currentItem=null; 

    if (exception && this._stopOnFirstFailure) {
      this._notifyEnd();
    } else {
      this._startNextUpdate() ;    //start next request
    }
  }

  // WILL: TODO: make these inhertied  //////////////////////////////////////

  this._observers = []; //we call these when download starts / stops / has error

  // WILL: If I need to make this async then consider http://devedge-temp.mozilla.org/toolbox/examples/2003/CCallWrapper/index_en.html
  this._notify = function(arg)  {
    VMC.log('DBG','Notify '+arg);
    for (var i = 0; i < this._observers.length; ++i) {
      var observer=this._observers[i];
      //WILL: This doesn't work. observer and i take their last values
      //WILL: setTimeout( function() { observer.observe(arg) } , 10 ); // call each objects obvserve method after 10ms
      observer.observe(arg); // make blocking calls
    }
  };
  
  this.addObserver = function (aObserver) {
    if (!aObserver.observe) { VMC.alert('addObserver: not an observer'); }
    this._observers.push(aObserver);
  };

  this.removeObserver = function (aObserver) {
    for (var i = 0; i < this._observers.length; ++i) {
      if (this._observers[i]==aObserver) {
	this._observers.splice(i,1); //remove this one item
	return;
      }
    }
    VMC.log('WARN','WILL: could not find '+aObserver+' in observer list. This could be because window was closed before observer had a chance to register');
  };

  ///////// WILL: end of observable /////////////////////////////////////

};


